I wanted to make a simple key-value server in Elixir - json in, json out, GET, POST, with an in-memory map. The point is to reinvent the wheel, and learn me some Elixir. My questions were: a) how do I build this without Phoenix and b) how do I persist state between requests in a functional language?
Learning new stuff is always painful, so this was frustrating at points and harder than I expected. But I want to emphasize that I did get it working, and do understand a lot more about how Elixir does things - the community posts and extensive documentation were great, and I didn’t have to bug anyone on StackOverflow or IRC or anything to figure all this out.
Here’s what the learning & development process sounded like from inside my head.
-
First, how do I make a simple JSON API without Phoenix? I tried several tutorials using Plug alone. Several of them were out of date / didn’t work. Finally found this one, which was up-to-date and got me going.
Ferret
was born! -
How do I reload code when I change things without manually restarting? Poked around and found the remix app.
-
Now I can take JSON in, but how do I persist across requests? I think we need a subprocess or something? That’s what CodeShip says, anyhow.
-
Okay, I’ve got an Agent. So, where do I keep the agent PID so it’s reusable across requests?
-
Well, where the heck does Plug keep session data? [3] That should be in-memory by default, right? Quickly, to the source code!
-
Hrm, well, that doesn’t tell me a lot. Guess it’s abstracted out, and in a language I’m still learning.
-
Maybe I’ll make a separate plug to initialize the agent, then dump it into the request bag-of-data?
Pretty sure
plug MyPlug, agent_thing: MyAgent.start_link
will work. Can store that in my Plug’s options, then add it toConn
so it’s accessible inside requests -
Does a plug’s
init/1
get called on every request, or just once? What about my Router’sinit/1
? Are things there memoized? -
Guess I’ll assume the results are stored and passed in as the 2nd arg to
call/2
in my plug. -
Wait, what does
start_link
return?14:15:15.422 [error] Ranch listener Ferret.Router.HTTP had connection process started with :cowboy_protocol:start_link/4 at #PID<0.335.0> exit with reason: \{\{\%MatchError{term: [iix: {:ok, #PID<0.328.0>}]}
-
WHY DO I KEEP GETTING THIS?!
** (MatchError) no match of right hand side value: {:ok, #PID<0.521.0>} (ferret) lib/plug/load_index.ex:10: Ferret.Plug.LoadIndex.init/1
-
figures out how to assign arguments
turns out
[:ok, pid]
and{:ok, pid}
and%{"ok" => pid}
are different things -
futzes about trying various things to make that work
-
How do I log stuff, anyway? Time to learn Logger.
-
THE ROUTE IS RIGHT THERE WHAT THE HELL?!
14:29:45.127 [info] GET /put 14:29:45.129 [error] Ranch listener Ferret.Router.HTTP had connection process started with :cowboy_protocol:start_link/4 at #PID<0.716.0> exit with reason: \{\{\%FunctionClauseError{arity: 4,
-
half an hour later - Oh, I’m doing a GET request when I routed it as POST. I’m good at programmering! I swear! I’m smrt!
-
Turns out
Conn.assign/3
andconn.assigns
are how you put things in a request - notConn.put_private/3
like plug/session uses. -
Okay, I’ve got my module in the request, and the pid going into my KV calls
-
WTF does this mean?!?!
Ranch listener Ferret.Router.HTTP had connection process started with :cowboy_protocol:start_link/4 at #PID<0.298.0> exit with reason: {\{:noproc, {GenServer, :call, [#PID<0.292.0>,
-
bloody hours pass
-
The
pid
is right bloody there!Logger.debug
shows it’s passing in the same pid for every request! -
Maybe it’s keeping the
pid
around, but the process is actually dead? How do I figure that out? tries various things -
Know what’d be cool?
Agent.is_alive?
. Things that definitely don’t work:Process.get(pid_of_dead_process)
Process.get(__MODULE__)
Process.alive?(__MODULE__)
Which is weird, since an Agent is a GenServer is a Process (as far as I can tell). This article on “Process ping-pong” was helpful.
-
Finally figured out to use
GenServer.whereis/1
, passing in__MODULE__
, and that will returnnil
if the proc is dead, and info if it’s alive. -
Turns out I don’t need my own plug at all: just init the Agent with the
__MODULE__
name, and I can reference it by that, just like a GenServer. -
IT’S STILL SAYING
:noproc
! JEEBUS! -
Okay, I guess remix doesn’t re-run
Ferret.Router.init/1
when it reloads the code for the server. So when my Agent dies due to an ArgumentError or whatever, it never restarts and I get this:noproc
crap. -
I’ll just manually restart the server - I don’t want to figure out supervisors right now.
-
This seems like it should work, why doesn’t it work?
Agent.get_and_update __MODULE__, &Map.merge(&1, dict)
-
Is it doing a javascript async thing? Do I need to tell Plug to wait on the response to
get_and_update
? -
Would using
Agent.update
and thenAgent.get
work? Frick, I dunno, how async are we getting here? All. the. examples. use a pid instead of a module name to reference the agent. -
How would I even tell plug to wait on an async call?
-
Oh, frickin’!
get_and_update/3
has to return a tuple , and there’s no function that does single-return-value-equals-new-state.I need a function that takes the new map, merges it with the existing state, then duplicates the new map to return, but
get_and_update/3
‘s function argument only receives the current state and doesn’t get the arguments.get_and_update/4
supposedly passes args, but you have to pass a Module & atom instead of a function. I couldn’t make that work, either. -
Does Elixir have closures? I mean, that wouldn’t make a lot of sense from a “pure functions only” perspective, but in Ruby it’d be like
new_params = conn.body_params Agent.get_and_update do |state| new_state = Map.merge(state, new_params) [new_state, new_state] end
…errr, whelp, no, that doesn’t work.
-
The Elixir crash-course guide doesn’t mention closures, and I’m not getting how to do this from the examples.
-
hours of fiddling
-
uuuuugggghhhhhhhh functional currying monad closure pipe recursions are breaking my effing brain. You have to make your own curry, or use a library. This seems unnecessary for such a simple dang thing.
-
Is there a difference between
Tuple.duplicate(Map.merge(&1, dict), 2)
andMap.merge(&1, dict) |> Tuple.duplicate(2)
? I dunno, neither one of those are working. -
What’s the difference between?????
def myfunc do ... end ; &myfunc
f = fn args -> stuff end ; &f
&(do_stuff)
-
Okay, this is what I want:
&(Map.merge(&1, dict) |> Tuple.duplicate 2)
Why is
dict
available inside this captured function definition? I dunno. -
BOOM OMG IT’S WORKING! Programming is so cool and I’m awesome at it and this is the best!
-
Let’s
git commit
! -
Jeebus, I better write this crap down so I don’t forget it. Maybe someone else will find it useful. Wish I coulda Google’d this while I was futzing around.
-
I’m gonna go murder lots of monsters with my necromancer while my brain cools off. Then hopefully come back and figure out:
- functions and captures
- pipe operator’s inner workings
- closures???
- supervisors
Links I used:
Processes & State
Statefulness in a Stateful Language (CodeShip)
When to use Processes in Elixir
Concurrency Abstractions in Elixir (CodeShip)
GenServer name registration (hexdocs)
GenServer.whereis - for named processes
Agent.get_and_update (hexdocs) - hope you are good with currying: no way to pass args into the update function unless you can pass a module & atom (and that didn’t work for me)
Plug
How to build a lightweight webhook endpoint with Elixir
Plug (Elixir School) - intro / overview
Plug body_params - StackOverflow
plug/session.ex - how do they get / store session state?
Function Composition
Currying and Partial Application in Elixir
Elixir Crash Course - partial function applications